/**
 * web server.js node js 运行的一个web 服务器
 * 特点：
 * 1. 运行时指定端口 ： node ./web-server.js 5001
 * 2. 开启目录浏览,带参数?!dir： localhost:5001/pic/?!dir
 */
let http = require("http");
let url = require("url");
let fs = require("fs");
let path = require("path");

let config = {
	hostname: "0.0.0.0", //主机ip地址 127.0.0.1
	port: 5000, //端口值 8080
	home: "", //根目录地址，默认是当前web-server.js目录。默认值 '',
	//指定目录默认访问页
	defaultPages: ['index.html', 'index.asp', 'index.jsp', 'index.cshtml'],
	/**指定无法识别的格式的元类型 text/plain application/octet-stream（默认）*/
	unmatchFileAs: 'text/plain'
};

// 0 node 1 file
if (process.argv.length > 2) {
	config.port = process.argv[2];
}

if (config.hostname === "") {
	config.hostname = "0.0.0.0";
}

/**
 * 通过扩展名获取 mine type
 * @param {*} extName 扩展名 e.g. .txt .html
 * @returns 
 */
const getContentType = (extName) => {
	//from nginx mime types
	let mimeTypes = {
		".html": "text/html",
		".htm": "text/html",
		".shtml": "text/html",
		".css": "text/css",
		".xml": "text/xml",
		".gif": "image/gif",
		".jpeg": "image/jpeg",
		".jpg": "image/jpeg",
		".js": "application/javascript",
		".atom": "application/atom+xml",
		".rss": "application/rss+xml",
		".mml": "text/mathml",
		".txt": "text/plain",
		".gitignore": "text/plain",
		".jad": "text/vnd.sun.j2me.app-descriptor",
		".wml": "text/vnd.wap.wml",
		".htc": "text/x-component",
		".png": "image/png",
		".svg": "image/svg+xml",
		".svgz": "image/svg+xml",
		".tif": "image/tiff",
		".tiff": "image/tiff",
		".wbmp": "image/vnd.wap.wbmp",
		".webp": "image/webp",
		".ico": "image/x-icon",
		".jng": "image/x-jng",
		".bmp": "image/x-ms-bmp",
		".woff": "application/font-woff",
		".jar": "application/java-archive",
		".war": "application/java-archive",
		".ear": "application/java-archive",
		".json": "application/json",
		".hqx": "application/mac-binhex40",
		".doc": "application/msword",
		".pdf": "application/pdf",
		".ps": "application/postscript",
		".eps": "application/postscript",
		".ai": "application/postscript",
		".rtf": "application/rtf",
		".m3u8": "application/vnd.apple.mpegurl",
		".kml": "application/vnd.google-earth.kml+xml",
		".kmz": "application/vnd.google-earth.kmz",
		".xls": "application/vnd.ms-excel",
		".eot": "application/vnd.ms-fontobject",
		".ppt": "application/vnd.ms-powerpoint",
		".odg": "application/vnd.oasis.opendocument.graphics",
		".odp": "application/vnd.oasis.opendocument.presentation",
		".ods": "application/vnd.oasis.opendocument.spreadsheet",
		".odt": "application/vnd.oasis.opendocument.text",
		".pptx":
			"application/vnd.openxmlformats-officedocument.presentationml.presentation",
		".xlsx":
			"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
		".docx":
			"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
		".wmlc": "application/vnd.wap.wmlc",
		".7z": "application/x-7z-compressed",
		".cco": "application/x-cocoa",
		".jardiff": "application/x-java-archive-diff",
		".jnlp": "application/x-java-jnlp-file",
		".run": "application/x-makeself",
		".pl": "application/x-perl",
		".pm": "application/x-perl",
		".prc": "application/x-pilot",
		".pdb": "application/x-pilot",
		".rar": "application/x-rar-compressed",
		".rpm": "application/x-redhat-package-manager",
		".sea": "application/x-sea",
		".swf": "application/x-shockwave-flash",
		".sit": "application/x-stuffit",
		".tcltk": "application/x-tcl",
		".der": "application/x-x509-ca-cert",
		".pem": "application/x-x509-ca-cert",
		".crt": "application/x-x509-ca-cert",
		".xpi": "application/x-xpinstall",
		".xhtml": "application/xhtml+xml",
		".xspf": "application/xspf+xml",
		".zip": "application/zip",
		".msi": "application/octet-stream",
		".msp": "application/octet-stream",
		".msm": "application/octet-stream",
		".mid": "audio/midi",
		".midi": "audio/midi",
		".kar": "audio/midi",
		".mp3": "audio/mpeg",
		".ogg": "audio/ogg",
		".m4a": "audio/x-m4a",
		".ra": "audio/x-realaudio",
		".3gp": "video/3gpp",
		".3gpp": "video/3gpp",
		".ts": "video/mp2t",
		".mp4": "video/mp4",
		".mpeg": "video/mpeg",
		".mpg": "video/mpeg",
		".mov": "video/quicktime",
		".webm": "video/webm",
		".flv": "video/x-flv",
		".m4v": "video/x-m4v",
		".mng": "video/x-mng",
		".asx": "video/x-ms-asf",
		".asf": "video/x-ms-asf",
		".wmv": "video/x-ms-wmv",
		".avi": "video/x-msvideo",
		".gitignore": "text/plain",
		".gitattributes": "text/plain",
		"license": "text/plain"
	};
	let contentType = mimeTypes[extName];
	if (contentType === null || contentType === undefined) {
		contentType = config.unmatchFileAs;
	}
	return contentType;
};

/**
 * 获取请求路径
 * @param {IncomingMessage} req
 */
const getUrlObj = (req) => {
	let reqUrl = req.url;
	reqUrl = decodeURIComponent(reqUrl);
	let urlObj = url.parse(reqUrl);
	return urlObj;
};

/**
 * 格式化字节单位
 * @param {*} bytes
 * @param {*} decimals 小数点位数
 * @returns
 */
const formatBytes = (bytes, decimals = 2) => {
	if (bytes === 0) return "0 Bytes";
	const k = 1024;
	const dm = decimals < 0 ? 0 : decimals;
	const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
	const i = Math.floor(Math.log(bytes) / Math.log(k));
	return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
};

/**
 * 检测路径是否存在，同时判断是否是目录
 * @param {*} filePath 文件路径
 * @returns 
 */
const checkPathIsDirectory = (filePath) => {
	return fs.existsSync(filePath) &&
		fs.statSync(filePath).isDirectory();
}

/**
 * 检测路径是否存在，同时判断是否是文件
 * @param {*} filePath 文件路径
 * @returns 
 */
const checkPathIsFile = (filePath) => {
	return fs.existsSync(filePath) &&
		fs.statSync(filePath).isFile();
}

/**
 * 填充文本到预期长度
 * @param {*} text 
 * @param {*} expectLength 
 * @param {*} fillUnit 
 * @returns 
 */
const fillToExpectLength = (text, expectLength, fillUnit = "&nbsp;&nbsp;") => {
	let need = expectLength - text.length;
	if (need <= 0) {
		return text;
	}
	let append = "";
	for (let i = 0; i < need; i++) {
		append += fillUnit;
	}
	return text + append;
}

/**
 *
 * @param {*} filePath
 * @param {*} pathname 请求url上的路径名
 * @param {*} cb
 */
const buildDirectoryViewPage = (filePath, pathname, cb) => {
	let baseHtml =
		'<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />' +
		'<title>文件浏览</title><meta name="viewport" content="width=device-width, initial-scale=1.0" />' +
		"<style> * { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; }" +
		" .container { padding-right: 15px; padding-left: 15px; margin-right: auto; margin-left: auto; } " +
		"@media (min-width: 768px) { .container { width: 750px; } } @media (min-width: 992px) { .container " +
		"{ width: 970px; } } @media (min-width: 1200px) { .container { width: 1170px; } } .h1, h1 { " +
		"font-size: 36px; margin-top: 20px; margin-bottom: 10px; font-family: inherit; font-weight: 500; " +
		"line-height: 1.1; color: inherit; margin: 0.67em 0; } hr { margin-top: 20px; margin-bottom: 20px;" +
		" border: 0; border-top: 1px solid #eee; height: 0; -webkit-box-sizing: content-box; -moz-box-sizing:" +
		" content-box; box-sizing: content-box; } a {text-overflow: ellipsis;overflow:hidden;" +
		"display:inline-block;color: #337ab7; text-decoration: none; background-color:" +
		" transparent; }a:hover{text-decoration: underline;cursor:pointer;color:green;} " +
		".well { display: block; padding: 9.5px; margin: 0 0 10px; font-size: 13px; line-height:" +
		" 1.42857143; color: #333; word-break: break-all; word-wrap: break-word; background-color: #f5f5f5; border:" +
		" 1px solid #ccc; border-radius: 4px; } #fork { position: fixed; top: 0; right: 0; _position: absolute;" +
		" z-index: 10000; } .bottom { margin: 20px auto; width: 100%; text-align: center; } .container { min-width:" +
		" 800px; margin: 50px auto; } .well a{width:300px;} .date, .size { display: inline-block; min-width: 100px; margin-left: 100px;" +
		' } .title span,.title a{vertical-align: middle;}</style></head><body class="container">' +
		'<h1 class="title">[title]</h1><hr /><div class="well">[content]</div></body></html>';
	fs.stat(filePath, (err, stats) => {
		if (!stats.isDirectory()) {
			cb(`${filePath} is not a directory`);
			return;
		}
		let content = `<div><a href="/?!dir">根目录 /</a></div>`;
		if (pathname == "/") {
			content += `<div><a href="/?!dir">返回上一级 ..</a></div>`;
		} else {
			let newPathname = pathname.substring(0, pathname.length - 1);
			let idx = newPathname.lastIndexOf("/");
			newPathname = newPathname.substring(0, idx + 1);
			content += `<div><a href="${newPathname}?!dir">返回上一级 ..</a></div>`;
		}
		fs.readdir(filePath, (err, files) => {
			let sHideDirs = [];
			let sDirs = [];
			let sHideFiles = [];
			let sFiles = [];
			files.forEach((fileName) => {
				let stats = fs.statSync(path.join(filePath, fileName));
				if (stats.isDirectory()) {
					if (fileName.indexOf(".") == 0) {
						sHideDirs.push({
							name: fileName,
							mtime: fillToExpectLength(stats.mtime.toLocaleString(), 19),
							size: "-",
						});
					} else {
						sDirs.push({
							name: fileName,
							mtime: fillToExpectLength(stats.mtime.toLocaleString(), 19),
							size: "-",
						});
					}
				}
				if (stats.isFile()) {
					if (fileName.indexOf(".") == 0) {
						sHideFiles.push({
							name: fileName,
							mtime: fillToExpectLength(stats.mtime.toLocaleString(), 19),
							size: formatBytes(stats.size),
						});
					} else {
						sFiles.push({
							name: fileName,
							mtime: fillToExpectLength(stats.mtime.toLocaleString(), 19),
							size: formatBytes(stats.size),
						});
					}
				}
			});
			sHideDirs.sort();
			sDirs.sort();
			sHideDirs = sHideDirs.concat(sDirs);
			sHideFiles.sort();
			sFiles.sort();
			sHideFiles = sHideFiles.concat(sFiles);

			for (let i = 0; i < sHideDirs.length; i++) {
				let f = sHideDirs[i];
				content += `<div><a href="${pathname + f.name}?!dir" title="${f.name
					}">[D]${f.name}</a ><span class="date">${f.mtime
					}</span><span class="size">-</span></div>`;
			}
			for (let i = 0; i < sHideFiles.length; i++) {
				let f = sHideFiles[i];
				content += `<div><a href="${pathname + f.name}"  title="${f.name
					}">${f.name}</a ><span class="date">${f.mtime
					}</span><span class="size">${f.size}</span></div>`;
			}
			let arr = ["<span>Index Of </span>"];
			let pathArr = pathname.split("/");
			let basePath = "/";
			for (let i = 0; i < pathArr.length; i++) {
				const p = pathArr[i];
				if (p.length === 0) {
					continue;
				}
				basePath += p + "/";
				arr.push(`<a href="${basePath}?!dir">${p}</a>`);
			}
			let html = baseHtml.replace("[title]", arr.join("<span>/</span>"));
			html = html.replace("[content]", content);
			cb(html);
		});
	});
};

/**
 * 获取默认页
 * @param {*} filePath 
 */
const getDefaultPage = (filePath) => {
	if (!checkPathIsDirectory(filePath)) { return; }
	//如果当前文件夹是目录，就访问默认页
	let defaultPages = config.defaultPages;
	if (defaultPages.length === 0) {
		defaultPages = ["index.html"];
	}
	for (let i = 0; i < defaultPages.length; i++) {
		const defaultPage = defaultPages[i];
		let newFilePath = path.join(filePath, defaultPage);
		if (checkPathIsFile(newFilePath)) {
			return newFilePath;
		}
	}
	return filePath;
}

/**
 * 获取扩展名
 * @param {String} filePath 
 */
const getExtname = (filePath) => {
	filePath = filePath.toLowerCase();
	if (!checkPathIsFile(filePath)) { return; }
	let pathObj = path.parse(filePath);
	if (pathObj.ext === "") {
		return pathObj.base;
	}
	return pathObj.ext;
}

/**
 * 处理301永久跳转
 * @param {IncomingMessage} req
 * @param {ServerResponse} res
 */
const process301Redirect = (req, res) => {
	let urlObj = getUrlObj(req);
	let pathname = urlObj.pathname;
	let search = urlObj.search;
	if (search === null) {
		search = "";
	}
	let filePath = path.join(".", config.home, pathname);
	/**
	 * 跳转情况：
	 * 1.请求路径为空
	 * 2.判断请求路径是不是【文件夹】，如果是文件夹，
	 * 同时请求末尾不是 / 结尾，跳转到有 / 的路径
	 */
	let cond1 = pathname.length === 0;
	let cond2 = checkPathIsDirectory(filePath) &&
		(pathname.lastIndexOf("/") !== pathname.length - 1);
	if (cond1 || cond2) {
		let newPathname = pathname + "/" + search;
		console.log(
			`[info]:redirect url:from ${pathname + search} to ${newPathname}`
		);
		res.writeHead(301, { Location: newPathname });
		res.end();
		return true;
	}
	return false;
};

/**
 * 处理目录浏览 在路径末尾加 #!dir 即可进行目录浏览
 * @param {IncomingMessage} req
 * @param {ServerResponse} res
 */
const processDirectoryView = (req, res) => {
	let urlObj = getUrlObj(req);
	let pathname = urlObj.pathname;
	let filePath = path.join(".", config.home, pathname);
	if (
		checkPathIsDirectory(filePath) &&
		urlObj.query === "!dir"
	) {
		console.log(`[info]:open directory:${filePath}`);

		res.writeHead(200, { "Content-Type": "text/html" });
		buildDirectoryViewPage(filePath, pathname, (html) => res.end(html));
		return true;
	}
	return false;
};

/**
 * 处理MarkDown文件,请求url带有 !skip 跳过,如 /a.md?!skip
 * @param {IncomingMessage} req
 * @param {ServerResponse} res
 */
const processMarkDownFile = (req, res) => {
	let urlObj = getUrlObj(req);
	let pathname = urlObj.pathname;
	let filePath = path.join(".", config.home, pathname);
	if (!checkPathIsFile(filePath) || urlObj.query === "!skip") {
		return false;
	}
	let pathObj = path.parse(filePath);
	if (!(pathObj.ext === ".md" || pathObj.ext === ".markdown")) {
		return false;
	}
	res.writeHead(301, { Location: `/index.html?p=${pathname}` });
	res.end();
	return true;
};

/**
 * 处理文件定位
 * @param {IncomingMessage} req
 * @param {ServerResponse} res
 */
const processFileLocate = (req, res) => {
	let urlObj = getUrlObj(req);
	let pathname = urlObj.pathname;
	let filePath = path.join(".", config.home, pathname);
	//判断是否是个目录
	if (checkPathIsDirectory(filePath)) {
		filePath = getDefaultPage(filePath);
	}
	if (!checkPathIsFile(filePath)) {
		res.writeHead(404, { "Content-Type": "text/html" });
		res.end(`<h1>404 Not Found</h1><h2>path:${filePath}</h2`);
		return false;
	}
	let extname = getExtname(filePath);
	//在返回头中写入内容类型
	res.writeHead(200, {
		"Content-Type": getContentType(extname) + ";charset=utf-8",
	});
	//创建只读流用于返回
	let stream = fs.createReadStream(filePath, {
		flags: "r",
		encoding: null,
	});

	stream.on("error", () => {
		res.writeHead(404, { "Content-Type": "text/html" });
		res.end(`<h1>404 Read File Error</h1><h2>path:${filePath}</h2`);
	});

	//连接文件流和http返回流的管道,用于返回实际Web内容
	stream.pipe(res);
	return true;
};

const server = http.createServer((req, res) => {
	let reqUrl = decodeURIComponent(req.url);
	console.log(`[info]: request url: ${reqUrl}`);
	if (process301Redirect(req, res)) {
		return;
	}
	if (processDirectoryView(req, res)) {
		return;
	}
	if (processMarkDownFile(req, res)) {
		return;
	}
	processFileLocate(req, res);
});

server.on("error", (error) => {
	console.log(`[error]: ${error}`);
});

server.listen(config.port, config.hostname, () => {
	console.log("/******************** web server ********************/\n");
	console.log(`Server running at http://${config.hostname}:${config.port}/`);
	console.log("press Ctrl+C to stop server.");
	console.log("\n/****************** web server **********************/");
});
